package com.katesoft.scale4j.agent;

import com.katesoft.scale4j.common.io.FileUtility;
import com.katesoft.scale4j.common.lang.OS;
import com.katesoft.scale4j.common.lang.RuntimeUtility;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.xml.jvmcluster.ApplicationArgType;
import com.katesoft.xml.jvmcluster.ApplicationConfigurationDocument.ApplicationConfiguration;
import com.katesoft.xml.jvmcluster.JvmArgType;
import com.katesoft.xml.jvmcluster.JvmConfigurationDocument.JvmConfiguration;
import com.katesoft.xml.jvmcluster.JvmConfigurationDocument.JvmConfiguration.VmType;
import com.katesoft.xml.jvmcluster.ServiceDocument.Service;
import com.katesoft.xml.jvmcluster.SystemPropertiesType;
import org.apache.commons.lang.ArrayUtils;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static com.katesoft.scale4j.common.io.FileUtility.PATH_SEPARATOR;
import static com.katesoft.scale4j.common.io.FileUtility.SEPARATOR;

/**
 * Similar to {@link ProcessBuilder}, this class makes the java process creation easier.
 *
 * @author kate2007
 */
public class JavaProcessBuilder
{
    private Logger logger = LogFactory.getLogger(getClass());
    private File javaHome = FileUtility.JAVA_HOME;
    private File workingDir = FileUtility.USER_DIR;
    private List<String> classpath = new LinkedList<String>();
    private List<String> endorsedDirs = new LinkedList<String>();
    private List<String> extDirs = new LinkedList<String>();
    private List<String> libraryPath = new LinkedList<String>();
    private List<String> bootClasspath = new LinkedList<String>();
    private List<String> appendBootClasspath = new LinkedList<String>();
    private List<String> prependBootClasspath = new LinkedList<String>();
    private List<String> jvmArgs = new LinkedList<String>();
    private List<String> args = new LinkedList<String>();
    private Map<String, String> systemProperties = new HashMap<String, String>();
    private String initialHeap;
    private String maxHeap;
    private String vmType;
    private String mainClass;
    private boolean debugSuspend;
    private int debugPort = -1;

    public JavaProcessBuilder()
    {
        super();
    }

    public JavaProcessBuilder(ApplicationConfiguration cfg,
                              Service service)
    {
        mainClass(service.getMainClass());
        applyJvmConfiguration(service.getJvmConfiguration());
        if (!ArrayUtils.isEmpty(service.getApplicationArgArray())) {
            for (ApplicationArgType arg : service.getApplicationArgArray()) {
                arg(arg.getValue());
            }
        }
        if (cfg.getGlobalConfiguration() != null) {
            addJvmArgs(cfg.getGlobalConfiguration().getJvmArgArray());
            addSystemProperties(cfg.getGlobalConfiguration().getSystemPropertyArray());
        }
        systemProperty("com.sun.management.jmxremote.port", String.valueOf(service.getJmxPort().intValue()));
        systemProperty("com.sun.management.jmxremote", null);
    }

    public void applyJvmConfiguration(JvmConfiguration jvmConfiguration)
    {
        if (jvmConfiguration != null) {
            if (jvmConfiguration.getMemoryConfiguration() != null) {
                if (jvmConfiguration.getMemoryConfiguration().isSetInitialHeap()) {
                    initialHeap(jvmConfiguration.getMemoryConfiguration().getInitialHeap().intValue());
                }
                if (jvmConfiguration.getMemoryConfiguration().isSetMaxHeap()) {
                    maxHeap(jvmConfiguration.getMemoryConfiguration().getMaxHeap().intValue());
                }
            }
            if (jvmConfiguration.getBootClasspathConfiguration() != null) {
                if (jvmConfiguration.getBootClasspathConfiguration().isSetPrependBootClasspath()) {
                    prependBootClasspath(jvmConfiguration.getBootClasspathConfiguration().getPrependBootClasspath());
                }
                if (jvmConfiguration.getBootClasspathConfiguration().isSetBootClasspath()) {
                    bootClasspath(jvmConfiguration.getBootClasspathConfiguration().getBootClasspath());
                }
                if (jvmConfiguration.getBootClasspathConfiguration().isSetAppendBootClasspath()) {
                    appendBootClasspath(jvmConfiguration.getBootClasspathConfiguration().getAppendBootClasspath());
                }
            }
            if (jvmConfiguration.getExtPathConfiguration() != null) {
                if (jvmConfiguration.getExtPathConfiguration().isSetEndorsedDirs()) {
                    endorsedDir(jvmConfiguration.getExtPathConfiguration().getEndorsedDirs());
                }
                if (jvmConfiguration.getExtPathConfiguration().isSetExtDirs()) {
                    extDir(jvmConfiguration.getExtPathConfiguration().getExtDirs());
                }
                if (jvmConfiguration.getExtPathConfiguration().isSetLibraryPath()) {
                    libraryPath(jvmConfiguration.getExtPathConfiguration().getLibraryPath());
                }
            }
            if (jvmConfiguration.getDebugConfiguration() != null) {
                debugPort(jvmConfiguration.getDebugConfiguration().getPort());
                debugSuspend(jvmConfiguration.getDebugConfiguration().getDebugSuspend());
            }
            if (jvmConfiguration.getVmType() != null) {
                if (VmType.SERVER == jvmConfiguration.getVmType()) {
                    server();
                }
                else if (VmType.CLIENT == jvmConfiguration.getVmType()) {
                    client();
                }
            }
            if (jvmConfiguration.isSetAdditionalClasspath()) {
                classpath(jvmConfiguration.getAdditionalClasspath());
            }
            addJvmArgs(jvmConfiguration.getJvmArgArray());
            addSystemProperties(jvmConfiguration.getSystemPropertyArray());
        }
    }

    private void addJvmArgs(JvmArgType[] jvmArgArray)
    {
        if (!ArrayUtils.isEmpty(jvmArgArray)) {
            for (JvmArgType arg : jvmArgArray) {
                jvmArg(arg.getValue());
            }
        }
    }

    private void addSystemProperties(SystemPropertiesType[] array)
    {
        if (!ArrayUtils.isEmpty(array)) {
            for (SystemPropertiesType arg : array) {
                systemProperty(arg.getName(), arg.getValue());
            }
        }
    }

    public File javaHome()
    {
        return javaHome;
    }

    public JavaProcessBuilder workingDir(String dir)
    {
        return workingDir(new File(dir));
    }

    public JavaProcessBuilder workingDir(File dir)
    {
        workingDir = dir;
        return this;
    }

    public JavaProcessBuilder classpath(String resource)
    {
        classpath.add(resource);
        return this;
    }

    public JavaProcessBuilder endorsedDir(String dir)
    {
        endorsedDirs.add(dir);
        return this;
    }

    public JavaProcessBuilder extDir(String dir)
    {
        extDirs.add(dir);
        return this;
    }

    public JavaProcessBuilder libraryPath(String dir)
    {
        libraryPath.add(dir);
        return this;
    }

    public JavaProcessBuilder bootClasspath(String resource)
    {
        bootClasspath.add(resource);
        return this;
    }

    public JavaProcessBuilder appendBootClasspath(String resource)
    {
        appendBootClasspath.add(resource);
        return this;
    }

    public JavaProcessBuilder prependBootClasspath(String resource)
    {
        prependBootClasspath.add(resource);
        return this;
    }

    public JavaProcessBuilder systemProperty(String name,
                                             String value)
    {
        systemProperties.put(name, value);
        return this;
    }

    public JavaProcessBuilder initialHeap(int mb)
    {
        return initialHeap(mb + "m");
    }

    public JavaProcessBuilder initialHeap(String size)
    {
        initialHeap = size;
        return this;
    }

    public JavaProcessBuilder maxHeap(int mb)
    {
        return maxHeap(mb + "m");
    }

    public JavaProcessBuilder maxHeap(String size)
    {
        maxHeap = size;
        return this;
    }

    public JavaProcessBuilder client()
    {
        vmType = "-client";
        return this;
    }

    public JavaProcessBuilder server()
    {
        vmType = "-server";
        return this;
    }

    public JavaProcessBuilder jvmArg(String arg)
    {
        jvmArgs.add(arg);
        return this;
    }

    public JavaProcessBuilder mainClass(String mainClass)
    {
        this.mainClass = mainClass;
        return this;
    }

    public JavaProcessBuilder debugSuspend(boolean suspend)
    {
        this.debugSuspend = suspend;
        return this;
    }

    public JavaProcessBuilder debugPort(int port)
    {
        this.debugPort = port;
        return this;
    }

    public JavaProcessBuilder arg(String arg)
    {
        args.add(arg);
        return this;
    }

    private static String toString(Iterable<String> paths) throws IOException
    {
        StringBuilder buff = new StringBuilder();
        for (String p : paths) {
            if (buff.length() > 0 && !p.endsWith(PATH_SEPARATOR)) { buff.append(PATH_SEPARATOR); }
            buff.append(p);
        }
        return buff.toString();
    }

    public String[] command() throws IOException
    {
        List<String> cmd = new ArrayList<String>();
        String executable = javaHome.getCanonicalPath() + SEPARATOR + "bin" + SEPARATOR + "java";
        if (OS.get().isWindows()) { executable += ".exe"; }
        cmd.add(executable);
        String path = toString(prependBootClasspath);
        if (!path.isEmpty()) { cmd.add("-Xbootclasspath/p:" + path); }
        path = toString(bootClasspath);
        if (!path.isEmpty()) { cmd.add("-Xbootclasspath:" + path); }
        path = toString(appendBootClasspath);
        if (!path.isEmpty()) { cmd.add("-Xbootclasspath/a:" + path); }
        path = toString(classpath);
        if (!path.isEmpty()) {
            cmd.add("-classpath");
            cmd.add(path);
        }
        path = toString(extDirs);
        if (!path.isEmpty()) { cmd.add("-Djava.ext.dirs=" + path); }
        path = toString(endorsedDirs);
        if (!path.isEmpty()) { cmd.add("-Djava.endorsed.dirs=" + path); }
        path = toString(libraryPath);
        if (!path.isEmpty()) { cmd.add("-Djava.library.path=" + path); }
        //
        for (Map.Entry<String, String> prop : systemProperties.entrySet()) {
            if (prop.getValue() == null) { cmd.add("-D" + prop.getKey()); }
            else { cmd.add("-D" + prop.getKey() + "=" + prop.getValue()); }
        }
        if (initialHeap != null) { cmd.add("-Xms" + initialHeap); }
        if (maxHeap != null) { cmd.add("-Xmx" + maxHeap); }
        if (vmType != null) { cmd.add(vmType); }
        if (debugPort > 0) {
            cmd.add("-Xdebug");
            cmd.add("-Xnoagent");
            cmd.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=" + (debugSuspend ? 'y' : 'n') + ",address=" + debugPort);
        }
        cmd.addAll(jvmArgs);
        if (mainClass != null) {
            cmd.add(mainClass);
            cmd.addAll(args);
        }
        return cmd.toArray(new String[cmd.size()]);
    }

    /**
     * launches jvm with current configuration.
     * <p/>
     * note that, the streams passed are not closed automatically.
     *
     * @param output outputstream to which process's input stream to be redirected. if null, it is not redirected
     * @param error  outputstream to which process's error stream to be redirected. if null, it is not redirected
     * @return the process created
     * @throws IOException if an I/O error occurs.
     */
    public Process launch(OutputStream output,
                          OutputStream error) throws IOException
    {
        String[] command = command();
        logger.info("working_dir = %s, command = %s", workingDir.getCanonicalPath(), Arrays.toString(command));
        ProcessBuilder processBuilder = new ProcessBuilder();
        processBuilder.command(command);
        processBuilder.directory(workingDir);
        Process process = processBuilder.start();
        RuntimeUtility.redirectStreams(process, output, error);
        return process;
    }
}
